iT邦幫忙

2023 iThome 鐵人賽

DAY 24
0
自我挑戰組

Concurrency in go 讀書心得系列 第 24

24.Timeout and Cancellation

  • 分享至 

  • xImage
  •  

在編寫併發代碼時,超時和取消會頻繁出現。
那麽,我們為什麽需要併發程序支持超時呢?
系統飽和
正如我們在“隊列”部分所討論的那樣,如果系統已經達到最大負荷(即,它的處理請求的能力達到了極
限),我們可能希望系統的請求超時而不是花很長時間等待。你選擇哪條路線取決於你的實際業務,但這
里有一些關於何時觸發超時的一般性指導:
1.如果請求在超時情況下不太可能重覆發送。
2.如果沒有資源來存儲請求(例如,臨時隊列的內存,持久隊列的磁盤空間)。
3.如果請求或其發送的數據的過期(我們將在下面討論)。
4.如果一個請求可能會重覆發生,那麽系統 將會接受並超時請求。 如果開銷超過我們系統的容量,這可能導致死亡螺旋。
但是,如果我們缺乏將請求存儲在隊列中所需的系統資源,這是一個有爭議的問題。
即使我們符合這兩條準則,將該請求加入隊列也沒有什麽意義,只要我們可以處理請求,請求就會過期。這給我們帶來了超時的下一個理由。
**數據過期 **
有時數據有一個窗口,在這個窗口中必須優先處理部分相關數據,或者處理數據的需求已過期。如果一個併發進程比這個窗口花費更長的時間來處理數據,我們希望超時並取消該進程。例如,如果某個併發進程 在長時間等待後發起請求,則在隊列中請求或其數據可能已過期。
如果這個窗口已經事先知曉,那麽將context.WithDeadline或context.WithTimeout創建的 context.Context傳遞給我們的併發進程是有意義的。
如果不是,我們希望併發進程的父節點能夠在需求不再需要時取消併發進程。context.WithCancel完美適用於此目的。
防止死鎖
在大型系統中,尤其是分布式系統中,有時難以理解數據流動的方式或系統邊界可能出現的情況。這並非毫無道理,甚至有人建議將超時放置在所有併發操作上,以確保系統不會發生死鎖。 超時時間不一定要接近執行併發操作所需的實際時間。設置超時時間的目的僅僅是為了防止死鎖,所以它只需要足夠短以滿足 死鎖系統會在合理的時間內解鎖即可。 我們在“死鎖,活鎖和鎖的饑餓問題”章節中提到過,通過設置超時來避免死鎖可能會將問題從死鎖變為 活鎖。在大型系統中,由於存在更多的移動部件,因此與死鎖相比,系統遇到不同的時序配置文件的可能 性更大。因此,最好有機會鎖定並修覆進程,而非直接讓系統死鎖最終不得不重啟。 請注意,這不是關於如何正確構建系統的建議。而是建議你考思考在開發和測試期間的時間、時序問題。 我建議你使用超時,但是目標應該集中在一個沒有死鎖的系統上,在這種系統中,超時基本不會觸發。
現在我們了解了何時使用超時,讓我們將注意力轉向取消,以及如何構建併發進程以優雅地處理取消。並
發進程可能被取消的原因有很多:
超時
超時是隱式的取消操作。
用戶干預
為了獲得良好的用戶體驗,通常建議如果啟動長時間運行的進程時,向服務器做輪詢將狀態報告給用戶,
或允許用戶查看他們的狀態。當面向用戶的併發操作時,有時需要允許用戶取消他們已經開始的操作。
父節點取消
如果作為子節點的任何父節點停止,我們應當執行取消。
重覆請求
我們可能希望將數據發送到多個併發進程,以嘗試從其中一個進程獲得更快的響應。
當收到響應時,需要取消其餘的處理。 我們將在“重覆請求”一節中詳細討論。
此外,也可能有其他的原因。然而,“為什麽”這個問題並不像“如何”這樣的問題那麽困難或有趣。在 第4章中,我們探討了兩種取消併發進程的方法:使用done通道和context.Context類型。 但這里我們要 探索更覆雜的問題:當一個併發進程被取消時,這對正在執行的算法及其下遊消費者意味著什麽?在編寫 可隨時終止的併發代碼時,需要考慮哪些事項?
為了回答這些問題,我們需要探索的第一件事是併發進程的可搶占性。下面是一個簡單的例子:

var value interface{}
  select {
  case <-done:
      return
  case value = <-valueStream:
  }
  result := reallyLongCalculation(value)
  select {
  case <-done:
      return
  case resultStream <- result:
  }

我們已經將valueStream的讀取和resultStream的寫入耦合起來,並檢查done通道,看看goroutine是否已被取消,但這裡存在問題。reallyLongCalculation看起來並不會執行搶占操作,而且根據名字,它看起來可能需要很長時間。這意味著,如果在reallyLongCalculation正在執行時某些事件試圖取消這個 goroutine,則可能需要很長時間才能確認取消並停止。讓我們試著讓reallyLongCalculation搶占進程,看看會發生什麽:

reallyLongCalculation := func(done <-chan interface{}, value interface{}) interface{ }{
      intermediateResult := longCalculation(value)
             select {
      case <-done:
          return nil
      default:
}
      return longCaluclation(intermediateResult)
  }

我們已經取得了一些進展:reallyLongCalculation現在可以搶占進程。但問題依然存在:我們只能對調用 該函數的地方進行搶占。為了解決這個問題,我們需要繼續調整

reallyLongCalculation := func(done <-chan interface{}, value interface{}) interface{ }{
      intermediateResult := longCalculation(done, value)
      return longCaluclation(done, intermediateResult)
  }

如果將這一推理結果歸納一下,我們會看到當前必須做兩件事:定義併發進程可搶占的時間段,並確保任何花費比此時間段更多時間的函數本身是可搶占的。一個簡單的方法就是將你的goroutine分解成更小的部分。
你應該瞄準所有不可搶占的原子操作,以便在更短的時間內完成


上一篇
23.Error-propagation
下一篇
25.Heartbeat
系列文
Concurrency in go 讀書心得30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言